Text(
"文字",
style: TextStyle(color: Colors.red, fontSize: 14),
textAlign: TextAlign.center
)
注意到 Text
建構子的組成。第一個參數是顯示的文字字串,是位置參數。後續為具名參數,Flutter 傾向使用具名參數來確保 Widget 結構的可讀性。一些基礎參數或必填參數,例如這裡的「文字」則經常為第一個位置參數。
Image
用於顯示圖片,截至目前為止 Widget 支援 jpeg, png, gif, webp, bmp, wbmp
Image(
image: AssetImage('images/2.png'),
width: 100,
height: 100,
)
Image
須指定圖片來源,可以從網路讀取,本地裝置,或應用程式定義的資源。為了管理不同的資源,Widget 有一個 image
屬性可以指定 ImageProvider
類型。上面範例的 AssetImage
提供者由 app 資源。如果要讀取網路圖片則是使用 NetworkImage
,如果是裝置檔案則使用 FileImage
。
此外,Image
也支援一些方便的建構子:
Image.asset
:這會建立一個 AssetImage
提供者類型的圖片,可以讀起 app 的資源圖片。
Image.asset('images/logo.png')
Image.network
:NetworkImage
提供者類型的圖片,可從 URL 讀取圖片
Image.network('https://picsum.photos/250?image=9')
Image.file
:FileImage
提供者類型的圖片
Image.file(File(file_path))
注意,若是本地資源檔案,則須先至 pubspec.yaml
設定。
flutter:
uses-material-design: true
# 加入資源檔案則如下:
assets:
- images/1.png
- images/2.png
Flutter 中許多 Widget 某種程度都跟特定平台的設計規範有關:如 Material Design 或 iOS Cupertino。這也讓開發者容易遵循平台的設計規範。
舉例來說 Flutter 沒有 Button
而是由 Google Material Design 和 iOS Cupertino 提供按鈕。我們可以簡單的選擇其中一個實作。實務上不用根據執行平台來切換風格。
一個對話視窗覆蓋在當前的介面也就是 Modal 效果且後面搭配半透明深色遮罩。在提供資訊、警告或提示錯誤時非常使用。
Flutter 提供了 Material Design 和 Cupertino 兩種。包含 Material Design 的 SimpleDialog
和 AlertDialog
以及 Cupertino 的 CupertinoAlertDialog
。
TextButton(
onPressed: () => showDialog<String>(
context: context,
builder: (BuildContext context) => Dialog(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('基本對話框'),
const SizedBox(height: 15),
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('關閉'),
),
],
),
),
),
),
child: const Text('顯示對話框'),
),
互動 Widget 在任何應用程式都是至關重要的環節,因為它們讓使用者可以操作你的 app 以及使 app 可以提供呈現資訊或者基於使用者的需求進行的動作。使用者有很多方式可以和 app 互動,在後續章節我們會深入介紹。這裡我們從一個最基礎的方式開始 Button
按鈕是一種可以接受互動的 Widget ,點擊並呼叫在其建構函式(constructor)中所提供的相關程式碼或方法。。Material Design 實作了:
FloatingActionButton
:如我們之前提到的,浮動動作按鈕是一個圓形,顯示圖示 ,一般漂浮在右下角,位置也可以設定調整。一般用於主要行為操作。例如一個顯示電子郵件訊息的也沒,這個按鈕可以是搭配一個 + Icon 功能是建立新郵件。TextButton
:文字按鈕為在 Material 元件上放置一串文字。使用者點擊時會產生 Material 波紋效果。ElevatedButton
:和文字按鈕很類似,凸起按鈕呈現些微懸浮的視覺效果。OutlinedButton
:輪廓按鈕也和文字按鈕很接近,但文字周圍有外框。IconButton
:圖示按鈕是在 Material 元件上顯示一個圖示。DropDownButton
:下拉按鈕類似於網站常見的下拉式選單。它顯示當前選取的項目,旁邊帶有箭頭符號。按下按鈕會展開一個選單,讓使用者選擇其他項目。PopUpMenuButton
:彈出式選單按鈕會在按下時顯示一個選單,包含多個選項讓使用者選擇。而針對 Cupertino ,Flutter 則提供 CupertinoButton
。
文字欄位讓使用者可以使用裝置的鍵盤輸入文字。Material Design 和 iOS Cupertino 兩個設計架構中都有實作 Text Field 對應分別是 TextField
和 CupertinoTextField
兩個元件都具有顯示虛擬鍵盤讓使用者輸入的功能。以下是一些它們共有的屬性:
autofocus
: 該屬性設定 TextField
顯示在畫面上時會自動聚焦enabled
:設定文字框是否可以編輯keyboardType
:變更鍵盤類型。例如如果你只希望使用者輸入數字,或者希望使用者輸入密碼時自動修正功能關閉。TextField(
decoration: InputDecoration(
hintText: '請輸入文字',
),
),
學習這些原廠組件最好的方式就是參考官方 API 文件,例如 TextField,你可以到文件上查詢說明和範例。我們需要花點時間掌握這些組件。
選擇類 Widget 可以讓使用者選擇選項。在 Material Design 系列中有:
Checkbox
可以選擇列表中多個選項Radio
可以選擇列表中的單一選項Switch
可以開關選項Slider
可以通過拖拉的方式選擇範圍中的值Cupertino 系列中:
CupertinoActionSheet
:這是一個 iOS 風格的 Modal 動作選單,可以先單選或多選CupertinoPicker
:選擇器控制可用於從一組較短的清單中選擇項目CupertinoSegmentedControl
:類似 Radio Button 支援單選CupertinoSlider
:類似 Material Design 的 Slider
CupertinoSwitch
:類似 Swtich
值得注意的是,混合搭配 Material Design 和 Cupertino 風格的元件是「完全沒有問題」的。如果你認為某個 Cupertino 元件看起來比 Material Design 元件更合適,那麼就使用它。
針對 Material Design,Flutter 通過 showDatePicker
和 showTimePicker
函式提供選擇器,其使用 Material Design 對話視窗建置。
而 Cupertino 則是提供 CupertinoDatePicker
和 CupertinoTimerPicker
Widget。
class DatePickerBox extends StatefulWidget {
@override
State<DatePickerBox> createState() => _DatePickerBoxState();
}
class _DatePickerBoxState extends State<DatePickerBox> {
DateTime selectedDate = DateTime.now();
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(2000),
lastDate: DateTime(2025),
);
if (picked != null && picked != selectedDate) {
setState(() {
selectedDate = picked;
});
}
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ElevatedButton(
onPressed: () => _selectDate(context),
child: Text('Select date'),
),
],
);
}
}
雖然剛從 React Native 過來一定會有很多覺得不夠直覺的部分,但隨著範例的學習即可慢慢掌握。
現在你已經知道了許多內建的 Widget 而自然而然下一個問題就是如何控制 Widget 的呈現。
從目前我們看到的 Widget 關於如何設定位置的方式或許不是很明顯,又或者如何根據螢幕的尺寸呈現。
例如:要在螢幕下方角落放置一個按鈕,我們可能以為可以根據螢幕的相對位置指定。但你可能注意到了我們的按鈕沒有位置相關的屬性。
那麼 Widget 是如何排版的?答案是更多的 Widget!Flutter 提供了一些用來組合佈局的 Widget 它們可以設定位置,大小,樣式等等。
單純呈現一個 Widget 在畫面上無法組織呈現介面。通常我們會使用一系列 Widget 來組成。
管理佈局最簡單的方式就是使用 Container
Widget
Container(
padding: EdgeInsets.all(14),
decoration: BoxDesoration(
border: Border.all(),
),
child: Text('Beautiful Teesside')
)
在這個範例中 Container
會有 14px 的內邊距並且有邊線,最後 Text
Widget 包含文字。Container
Widget 有一些實用的屬性例如:
padding
:設定容器內部邊距color
:容器背景色margin
:容器外部邊距decoration
:設定容器是否有背景圖片或背景色,周圍是否有邊框,是否有圓角。注意:我們不行同時設定 color
和 decoration
如果使用 decoration
那麼就要把 color
的設定移到 decoration
物件內height/width
:設定容器的寬高Flutter 還支援特殊的 Widget 基於一般的 Container
。包含支援動畫的 Container
後續在動畫章節會探討。
置中的效果可以通過 Center
Widget 達成:
Center(
child: Text('文字')
)
對齊子 Widget 和父層可以使用 Align
Align(
alignment: Alignment.topRight,
child: Text('文字')
)
廣泛使用的 Padding
可以指定子元素周圍的空間:
Padding(
padding: EdgeInsets.all(16.0),
child: Text('文字')
)
如你所見,當設定內邊距時,我們使用 EdgeInsets
類別。它包含了一些工廠建構子可以協助我們建立 EdgeInsets
物件。上面 EdgeInsets.all()
會為 4 邊設定邊距。
fromLTRB
建構子可以指定 4 週的邊距:
EdgeInsets.fromLTRB(left, top, right, bottom);
only
可以設定某一邊
EdgeInsets.only(right: 5);
symmetric
可以設定水平或垂直邊距:
EdgeInsets.symmetric(vertical: 8.0)
在實務中我們會很常看到 EdgeInsets
用在 padding
和 margin
屬性。
雖然 Container
更加通用且除了佈局設定位置還可以支援樣式控制。但怎麼做可能會讓程式碼可讀性和可維護性變差。例如置中需求我們可以就使用 Center
即可會比較易懂。
Flutter 中最常見的容器是 Row
和 Column
Widget。它們都支援 children
屬性,可以設定一個 Widget 列表。 Row
為水平排列,Column
為垂直排列。我們在前面的範例已經看過 Column
了,下面來看看 Row
的範例:
Row(
mainAxisAlignment: MainAxisAligment.spaceBetween,
children: [
Text('紅'),
Text('橙'),
Text('黃')
],
)
上面的例子,3 個字串會水平排列在螢幕上。
mainAxisAlignment
參數設定子元素在父元件中沿著主軸的間距佈局。以 Row
來說所謂的主軸是水平的。而 MainAxisAlignment.spaceBetween
標註子元素之間擁有相同的空間。
請注意,Row
並不擅長處理超出空間的情況,因此在小裝置中,我們需要額外添加 Widget 或屬性來確保不會破版:
Expanded
和 Flexible
可以讓子組件根據上層組件的空間調整大小。
Wrap
超出空間換行顯示。
SingleChildScrollView
搭配 crollDirection: Axis.horizontal
可以水平滾動。
LayoutBuilder
根據空間通過程式動態調整。
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth > 600) {
return Row(children: [...]);
} else {
return Column(children: [...]);
}
},
)
FittedBox
可以縮放其子 Widget 以適應可用空間。
Overflow
對於 Text
,你可以使用 overflow
屬性來處理文字溢出的情況。
接著,我們來看看 Column
的範例。語法和屬性非常類似 Row
但子元素會垂直排列:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
DestinationWidget(destinationName: 'Staithes'),
DestinationWidget(destinationName: 'Saltburn'),
DestinationWidget(destinationName: 'Whitby'),
],
)
crossAxisAlignment
屬性設定子元素如何沿著父元件中和主軸垂直的軸佈局間距。對 Column
而言,主軸是垂直的,也就交叉軸是水平方向。CrossAxisAlignment.start
會將所有子元件對齊到 Column
的左側,也就是軸的起點。
而 mainAxisSize
屬性指的是在 Column
的主軸方向上應該占用多少空間。對於 Column
而言,其主軸是垂直方向的,所以這裡的空間指的是高度。換言之,MainAxisSize.min
會使 Column
在垂直方向上的高度盡可能地小,僅足以容納其所有子元件。
你可能注意到了,我們在宣告列表或傳遞參數時,常會看到尾隨逗號。Dart 對於你的列表是否有尾隨逗號並不在意:對比 [item1, item2]
和 [item1, item2,]
。然而,有幾個理由你可能會選擇加上尾隨逗號。首先,如果列表已經存在一個逗號,那麼加入項目比較省事不會忘記。接著,複製和貼上時,因為有相同的語法比較不會出錯。第三,Dart 的程式碼格式化工具會把列表中的每一項自動排列到新的一行。這樣做的好處是讓代碼看起來更整潔、更容易閱讀。
另一個廣泛使用的 Widget 就是 Stack
,它可以按圖層的方式組織子元件,即一個子元件可以局部或完整重疊另一個子元件。這意味著,使用 Stack
,你可以將多個組件疊加在一起,形成一個層次結構。
類似於 Row
或 Column
可以傳入一個 children
子元件列表,但會根據順序疊起來。也就是第一個 Widget 會在堆疊的最下層,最後一個蓋在上面。
Stack(
children: [
Container(
width: 100,
height: 100,
color: Colors.red,
),
Container(
width: 50,
height: 50,
color: Colors.green,
),
Container(
width: 25,
height: 25,
color: Colors.blue,
),
]
)
紅色方塊會在最下面,藍色在最上面。
Scaffold
實作了 Material Design 或 Cupertino 的基本佈局結構。**一般來說我們通常會使用作為頁面的根節點。**主要是它支援了標準佈局格式,讓我們可以建立一致性和標準化的頁面結構。
在 Material Design Scaffold
可以包含多種 Material Design 元件如:
AppBar
:該 Widget 位於裝置螢幕的頂部。一般會在左邊有一個文字,右邊則是一些操作如按鈕等。TabBar
: 通常會在 AppBar
下面,可以水平切換幾個子頁面。TabBarView
:為了協助 TabBar
運作,通常需要定義幾個呈現頁面讓使用者切換。TabBarView
就是搭配頁籤切換的內容body
:頁面主要區塊。顯示在 AppBar
和 TabBar
下面幾乎涵蓋整個頁面。BottomNavigationBar
:位於裝置頁面的最下方導航列。使用者可以通過點擊不同的標籤來快速切換應用中的主要視圖。Drawer
:這是一個從側邊滑出的面板(導覽列),讓使用者可以快速切換頁面。通常在下方的為主要的切換例如在 Line 底下的選單。而 Drawer 則針對該頁面提供相關導航選單例如 Gamil 切換收件夾和垃圾郵件。然而 Flutter 並沒有限制該如何設計,你可以任意使用這些 Widget。在 iOS Cupertino ,頁面結構稍微不同,並且提供一些特定的轉換效果和行為。
關於 Cupertino 風格支援 CupertinoPageScaffold
和 CupertinoTabScaffold
通常搭配下面 Widget
CupertinoNavigationBar
置頂的導航列通常和 CupertinoPageScaffold
搭配使用CupertinoTabBar
底部的頁籤切換列,一般也是和 CupertinoPageScaffold
搭配使用如你所見,Cupertino 的 Scaffold 有比較多的限制。通常建議從 Material Design 的 Scaffold
入手,因為其涵蓋比較多功能可以滿足一個頁面大部分的需求。接著我們可以嘗試修改 Hello World 專案的 Scaffold
:
@override
Widget build(BuildContext) {
return Scaffold(
appBar: AppBar(
title: Text("標題")
),
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
DestinationWidget(destinationName: 'Staithes'),
DestinationWidget(destinationName: 'Saltburn'),
DestinationWidget(destinationName: 'Whitby'),
]
)
)
);
}
WidgetApp
是 Flutter 中的基本組件,支援了常見的 Widget 主要的核心功能就是將系統的「返回」綁定到 Navigator 操作或退出應用程式;返回、上一頁、退出這些功能。
而 MaterialApp
和 CupertinoApp
都是基於 WidgetApp
的功能實作的,例如程式碼中使用到 WidgetsApp.router
。無論使用 MaterialApp
或 CupertinoApp
風格基本功能都是使用 WidgetApp
實現。除了導覽器 WidgetApp
還支援了其他基礎功能如 Localization、無障礙、系統字體等等。
MaterialApp
是開發 Android 應用或者想要 Material Design 風格的應用的起點。它提供了許多基礎設施,比如導航、主題、國際化等。CupertinoApp
是為了開發符合 iOS 風格的應用而設計。它同樣提供導航、主題等基礎設施,但是風格和動畫效果都模仿 iOS。Scaffold
和 CupertinoScaffold
則是提供了頁面的基本結構在 Flutter 中,MaterialApp
的本質是一個整合 Widget ,若你需要遵循 Material Design ,MaterialApp
在 WidgetsApp
的基礎上增加了特定於 Material Design 的功能,比如 AnimatedTheme
和 GridPaper
。例如:
Theme
AnimatedTheme
GridPaper
預設 MaterialApp
會將 WidgetApp.textStyle
設定成很醜的紅黃樣式。這是為了提醒開發者你沒有做任何設定。一般情況下若搭配使用 Scaffold
那麼就會取得 MaterialApp.theme
的主題樣式。
MaterialApp
支援 Navigator
管理路由。也就是可以切換頁面,當使用者點擊按鈕或進行其他操作時,Navigator 會根據一定的規則來找到要顯示的下一畫面。
home
的情況下,造訪 /
時會顯示 home
。home
則查詢 routes
定義。過程中會檢查是否設定 onGenerateRoute
,也就是但沒有 home
也沒有定義 routes
時會使用 onGenerateRoute
onUnknownRoute
,這是最後的備選方案,用於處理未知的路由。如果 Navigator
被建立了,那麼至少要處理 /
路由,這是因為 Flutter 會嘗試使用這個無效的初始路由來找到一個匹配的頁面,如果沒有適當的配置來處理它,應用就會出現問題。
MaterialApp(
home: MyHomePage(),
routes: {
'/settings': (context) => SettingsPage(),
},
);
Flutter 也提供了 ListView
和 GridView
。ListView
比較接近 Column
但它可以捲動,且可以依據需求渲染。例如,我們的有個目的地列表項目超過 20 個,那麼一個畫面可能無法全部呈現,就會需要可捲動的功能。進一步但我們的 Widget 更複雜的時候,因為效能的考量我們不希望一口氣全部渲染,只有當 Widget 進入可視範圍才渲染。
ListView
支援捲動功能,通過 ListView.builder
建構子,當 Widget 需要顯示在畫面時才渲染。後續段落會有更詳細的介紹,現階段我們先嘗試使用 Column
來呈現大量的項目,然後看看 ListView
是如何運作的。
我們先在 Column
裡面放入 20 個 DestinationWidget
Padding(
padding: const EdgeInsets.all(16),
child: DestinationWidget("Staithes")
)
你應該會在下方看到 “黃黑線條”的警告區塊,並且提供訊息 Column
太長了超出畫面 BOTTOM OVERFLOW BY 100 PIXELS ,然後如果你嘗試要往下滑動,會注意到 Column
預設是無法捲動的。
接著,我們自己把 Column
替換成 ListView
。黃黑區塊和警告消失了,並且現在可以捲動了。這是一個簡單的 ListView
示範,希望帶來一個觀念,就是了解到需要考慮 Widget 適應不同情境需求。
GridView
也很類似,但是是建立柵格,而非列表,同時它也有和 ListView
相似的功能。
此外,還有其他不那麼常用,但也挺重要的 Widget 例如 Table
它以表格方式呈現。
每一個平台還有針對設計的 Widget。舉例來說 Material Design 有卡片的概念:一張卡片用於表示一些相關資訊。
另一方面,Cupertino 組件具有 iOS 特有的轉場效果。
你應該花時間探索,然後為你的應用程式決定設計標準。一致性很重要,如此才能提升使用者體驗。
Flutter 支援任何和 UI 相關的 Widget。舉例來說,手勢如捲動或點擊都和管理手勢的 Widge 有關。動畫和變形如縮放、旋轉也都由特定的 Widget 管理,後續我們會深入探討。
在 Flutter 中,SnackBar
用於顯示臨時的訊息,這些訊息會在螢幕底下彈出,還可以提供一些簡單的操作。但是 SnackBar
不能單獨存在通常需要和 Scaffold
搭配使用。
Scaffold
是一個提供了一個基本的 Material Design 佈局結構的 Widget。例如 AppBar
和右下角的懸浮按鈕 FloatingActionButton
,而 SnackBar
也是其中一種效果。Scaffold
本身會管理這些搭配 Widget 的佈局包含 SnackBar
顯示的空間。
Flutter 1.22 之後加入了 ScaffoldMessenger
目的在提供一個更好的方式顯示 SnackBar
。不管目前的 Scaffold
可不可以使用,它讓 SnackBar
可以支援多個 Scaffold
解決了例如在頁面切換的時候 SnackBar
無法保持顯示的問題。
一個最基本的例子:
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('哈囉'),
),
);
},
child: Text('問候')
);
但有時候在靜態方法或者事件 callback 中,我們沒有 context
可以使用。這個時候可以使用 GlobalKey<ScaffoldMessengerState>
首先,需要先建立一個全域 Key
final GlobalKey<ScaffoldMessengerState> snackBarKey = GlobalKey<ScaffoldMessengerState>();
然後搭配 MaterialApp
或 ScaffoldMessenger
等:
MaterialApp(
scaffoldMessengerKey: snackBarKey,
home: Scaffold(),
);
// 或
ScaffoldMessenger(
key: snackBarKey,
child: Scaffold(
...
),
);
最後即便沒有 context
我們也可以:
snackBarKey.currentState?.showSnackBar(
SnackBar(
content: Text('無 context 的 SnackBar'),
),
);
我們認識了一些基本的內建 Widget 包含了基本呈現,操作的 Widget 等等,然後關於佈局的 Widget 從 Container
到 ListView
學習了如何使用這些 Widget 建構設計基本的 app。現在我們對 Flutter 應用是如何構建的有了更好的理解;但還有很多東西需要了解。在下一章中,我們將進一步探討用戶如何與 Flutter 應用互動。關於這個章節,建議應該花更多的時間參考官方文件進行學習和實作好進一步深入的掌握 Flutter 。